package info.u250.c2d.box2deditor.ui.util;

import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

/*
 *  The DisablePanel will simulate the usage of a "Glass Pane" except that the
 *  glass pane effect is only for the container and not the entire frame.
 *  By default the glass pane is invisible. When the DisablePanel is disabled
 *  then the glass pane is made visible. In addition:
 *
 *  a) All MouseEvents will now be intercepted by the glass pane.
 *  b) The panel is removed from the normal focus traveral policy to prevent
 *     any component on the panel from receiving focus.
 *  c) Key Bindings for any component on the panel will be intercepted.
 *
 *  Therefore, the panel and components will behave as though they are disabled
 *  even though the state of each component has not been changed.
 *
 *  The background color of the glass pane should use a color with an
 *  alpha value to create the disabled look.
 *
 *  The other approach to disabling components on a Container is to recurse
 *  through all the components and set each individual component disabled.
 *  This class supports two static methods to support this functionality:
 *
 *  a) disable( Container ) - to disable all Components on the Container
 *  b) enable ( Container ) - to enable all Components disabled by the
 *     disable() method. That is any component that was disabled prior to using
 *     the disable() method method will remain disabled.
 */
public class DisabledPanel extends JPanel
{
	
	private static final long serialVersionUID = 1L;

	private static DisabledEventQueue queue = new DisabledEventQueue();

	private static Map<Container, List<JComponent>> containers = new HashMap<Container, List<JComponent>>();

	private	JComponent glassPane;
	
	private Container contentContainer;
	public Container getContentContainer(){
		return this.contentContainer;
	}

	/**
	 *	Create a DisablePanel for the specified Container. The disabled color
	 *  will be the following color from the UIManager with an alpha value:
	 *  UIManager.getColor("inactiveCaptionBorder");
	 *
	 *  @param container a Container to be added to this DisabledPanel
	 */
	public DisabledPanel(Container container)
	{
		this(container, null);
	}

	/**
	 *	Create a DisablePanel for the specified Container using the specified
	 *  disabled color.
	 *
	 *  @param disabledColor the background color of the GlassPane
	 *  @param container a Container to be added to this DisabledPanel
	 */
	public DisabledPanel(Container container, Color disabledColor)
	{
		contentContainer = container;
		setLayout( new OverlapLayout() );
		add( container );

		glassPane = new GlassPane();
		add( glassPane );

		if (disabledColor != null)
			glassPane.setBackground( disabledColor );
	}

	/**
	 *  The background color of the glass pane.
	 *
	 *  @return the background color of the glass pane
	 */
	public Color getDisabledColor()
	{
		return glassPane.getBackground();
	}

	/**
	 *  Set the background color of the glass pane. This color should
	 *  contain an alpha value to give the glass pane a transparent effect.
	 *
	 *  @param disabledColor the background color of the glass pane
	 */
	public void setDisabledColor(Color disabledColor)
	{
		glassPane.setBackground( disabledColor );
	}

	/**
	 *  The glass pane of this DisablePanel. It can be customized by adding
	 *  components to it.
	 *
	 *  @return the glass pane
	 */
	public JComponent getGlassPane()
	{
		return glassPane;
	}

	/**
	 *  Use a custom glass pane. You are responsible for adding the
	 *  appropriate mouse listeners to intercept mouse events.
	 *
	 *  @param glassPane a JComponent to be used as a glass pane
	 */
	public void setGlassPane(JComponent glassPane)
	{
		this.glassPane = glassPane;
	}

	/**
	 *	Change the enabled state of the panel.
	 */
	@Override
	public void setEnabled(boolean enabled)
	{
		super.setEnabled(enabled);

		if (enabled)
		{
			glassPane.setVisible(false);
			setFocusCycleRoot(false);
			queue.removePanel(this);
		}
		else
		{
			glassPane.setVisible(true);
			setFocusCycleRoot(true);  // remove from focus cycle
			queue.addPanel(this);
		}
	}

	/**
	 *  Because we use layered panels this should be disabled.
	 */
	@Override
	public boolean isOptimizedDrawingEnabled()
	{
		return false;
	}

	/**
	 *  Convenience static method to disable all components of a given
	 *  Container, including nested Containers.
	 *
	 *  @param container the Container containing Components to be disabled
	 */
	public static void disable(Container container)
	{
		List<JComponent> components =
			SwingUtils.getDescendantsOfType(JComponent.class, container, true);
		List<JComponent> enabledComponents = new ArrayList<JComponent>();
		containers.put(container, enabledComponents);

		for (JComponent component: components)
		{
			if (component.isEnabled())
			{
				enabledComponents.add( component );
				component.setEnabled(false);
			}
		}
	}

	/**
	 *  Convenience static method to enable Components disabled by using
	 *  the disable() method. Only Components disable by the disable()
	 *  method will be enabled.
	 *
	 *  @param container a Container that has been previously disabled.
	 */
	public static void enable(Container container)
	{
		List<JComponent> enabledComponents = containers.get( container );

		if (enabledComponents != null)
		{
			for (JComponent component: enabledComponents)
			{
				component.setEnabled(true);
			}

			containers.remove( container );
		}
	}

	/**
	 *  A simple "glass pane" that has two functions:
	 *
	 *  a) to paint over top of the Container added to the DisablePanel
	 *  b) to intercept mouse events when visible
	 */
	class GlassPane extends JComponent
	{
		private static final long serialVersionUID = 1L;

		public GlassPane()
		{
			setOpaque( false );
			setVisible( false );
			Color base = UIManager.getColor("inactiveCaptionBorder");
			base = (base == null) ? Color.LIGHT_GRAY : base;
			Color background = new Color(base.getRed(), base.getGreen(), base.getBlue(), 128);
			setBackground( background );

			//  Disable Mouse events for the panel

			addMouseListener( new MouseAdapter() {} );
			addMouseMotionListener( new MouseMotionAdapter() {} );
		}

		/*
		 *  The component is transparent but we want to paint the background
		 *  to give it the disabled look.
		 */
		@Override
		protected void paintComponent(Graphics g)
		{
			g.setColor( getBackground() );
			g.fillRect(0, 0, getSize().width, getSize().height);
		}
	}

	/**
	 *  A custom EventQueue to intercept Key Bindings that are used by any
	 *  component on a DisabledPanel. When a DisabledPanel is disabled it is
	 *  registered with the DisabledEventQueue. This class will check if any
	 *  components on the DisablePanel use KeyBindings. If not then nothing
	 *  changes. However, if some do, then the DisableEventQueue installs
	 *  itself as the current EquentQueue. The dispatchEvent() method is
	 *  overriden to check each KeyEvent. If the KeyEvent is for a Component
	 *  on a DisablePanel then the event is ignored, otherwise it is
	 *  dispatched for normal processing.
	 */
	static class DisabledEventQueue extends EventQueue
		implements WindowListener
	{
		private Map<DisabledPanel, Set<KeyStroke>> panels =
			new HashMap<DisabledPanel, Set<KeyStroke>>();

		/**
		 *  Check if any component on the DisabledPanel is using Key Bindings.
		 *  If so, then track the bindings and use a custom EventQueue to
		 *  intercept the KeyStroke before it is passed to the component.
		 */
		public void addPanel(DisabledPanel panel)
		{
			//  Get all the KeyStrokes used by all the components on the panel

			Set<KeyStroke> keyStrokes = getKeyStrokes( panel );

			if (keyStrokes.size() == 0) return;

			panels.put(panel, keyStrokes);

			//  More than one panel can be disabled but we only need to install
			//  the custom EventQueue when the first panel is disabled.

			EventQueue current = Toolkit.getDefaultToolkit().getSystemEventQueue();

			if (current != this)
				current.push(queue);

			//  We need to track when a Window is closed so we can remove
			//  the references for all the DisabledPanels on that window.

			Window window = SwingUtilities.windowForComponent( panel );
			window.removeWindowListener( this );
			window.addWindowListener( this );
		}

		/**
		 *  Check each component to see if its using Key Bindings
		 */
		private Set<KeyStroke> getKeyStrokes(DisabledPanel panel)
		{
			Set<KeyStroke> keyStrokes = new HashSet<KeyStroke>();

			//  Only JComponents use Key Bindings

			Container container = ((Container)panel.getComponent(1));
			List<JComponent> components =
				SwingUtils.getDescendantsOfType(JComponent.class, container);

			//  We only care about the WHEN_IN_FOCUSED_WINDOW bindings

			for (JComponent component: components)
			{
				InputMap im = component.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);

				if (im != null && im.allKeys() != null)
				{
					for (KeyStroke keyStroke: im.allKeys())
						keyStrokes.add( keyStroke );
				}
			}

			return keyStrokes;
		}

		/**
		 *	The panel is no longer disabled so we no longer need to intercept
		 *  its KeyStrokes. Restore the default EventQueue when all panels
		 *  using Key Bindings have been enabled.
		 */
		public void removePanel(DisabledPanel panel)
		{
			if (panels.remove(panel) != null
			&&  panels.size() == 0)
				pop();
		}

		/**
		 *  Intercept KeyEvents bound to a component on a DisabledPanel.
		 */
		@Override
		protected void dispatchEvent(AWTEvent event)
		{
			//  Potentially intercept KeyEvents

			if (event.getID() == KeyEvent.KEY_TYPED
			||  event.getID() == KeyEvent.KEY_PRESSED
			||  event.getID() == KeyEvent.KEY_RELEASED)
			{
				KeyEvent keyEvent = (KeyEvent)event;
				KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(keyEvent);

				//  When using Key Bindings, the source of the KeyEvent will be
				//  the Window. Check each panel belonging to the source Window.

				for (DisabledPanel panel: panels.keySet())
				{
					Window panelWindow = SwingUtilities.windowForComponent(panel);

					//  A binding was found so just return without dispatching it.

					if (panelWindow == keyEvent.getComponent()
					&&  searchForKeyBinding(panel, keyStroke))
						return;
				}
			}

			//  Dispatch normally

			super.dispatchEvent(event);
		}

		/**
		 *  Check if the KeyStroke is for a Component on the DisablePanel
		 */
		private boolean searchForKeyBinding(DisabledPanel panel, KeyStroke keyStroke)
		{
			Set<KeyStroke> keyStrokes = panels.get(panel);

			return keyStrokes.contains( keyStroke );
		}

		//  Implement WindowListener interface

		/**
		 *  When a Window containing a DisablePanel that has been disabled is
		 *  closed, remove the DisablePanel from the DisabledEventQueue. This
		 *  may result in the DisabledEventQueue deregistering itself as the
		 *  current EventQueue.
		 */
		public void windowClosed(WindowEvent e)
		{
			List<DisabledPanel> panelsToRemove = new ArrayList<DisabledPanel>();
			Window window = e.getWindow();

			//  Create a List of DisabledPanels to remove

			for (DisabledPanel panel: panels.keySet())
			{
   				Window panelWindow = SwingUtilities.windowForComponent(panel);

				if (panelWindow == window)
				{
					panelsToRemove.add( panel );
				}
			}

			//  Remove panels here to prevent ConcurrentModificationException

			for (DisabledPanel panel: panelsToRemove)
			{
				removePanel( panel );
			}

			window.removeWindowListener( this );
		}

		public void windowActivated(WindowEvent e) {}
		public void windowClosing(WindowEvent e) {}
		public void windowDeactivated(WindowEvent e) {}
		public void windowDeiconified(WindowEvent e) {}
		public void windowIconified(WindowEvent e) {}
		public void windowOpened(WindowEvent e) {}
	}
}
